use std::sync::mpsc::TryRecvError;
use std::sync::{mpsc, Arc};

use craftdig_datamodel::block::action;
use craftdig_datamodel::player::id::PlayerId;
use craftdig_datamodel::world::action::{PlayerInput, PlayerJoin, TickInput};
use craftdig_datamodel::world::{editable, WorldState};
use craftdig_math::Vec3f;

use crate::input;

/// Input message from the client.
pub enum Input {
    Action(action::Action),
}

/// Message sent back to the frontend containing information needed to update
/// the displayed world state (and later, reinitialize the dirty buffer).
pub struct TickMsg {
    /// Tick index.
    idx: u64,

    /// Reference to the new world state after computing the tick.
    new_state: Arc<WorldState>,

    /// The changes for the new state.
    changes: editable::TickChanges,

    /// Messages that should be logged to the player, if possible.
    msgs: Vec<String>,
}

impl TickMsg {
    pub fn new(
        idx: u64,
        new_state: Arc<WorldState>,
        changes: editable::TickChanges,
        msgs: Vec<String>,
    ) -> Self {
        Self {
            idx,
            new_state,
            changes,
            msgs,
        }
    }

    pub fn tick_idx(&self) -> u64 {
        self.idx
    }

    pub fn state(&self) -> &Arc<WorldState> {
        &self.new_state
    }

    pub fn changes(&self) -> &editable::TickChanges {
        &self.changes
    }

    pub fn msgs(&self) -> &[String] {
        &self.msgs
    }
}

/// Server control messages from some supervisor thread or something.
#[derive(Clone, Debug)]
pub enum ServerMsg {
    NewPlayer { id: PlayerId, loc: Vec3f },
}

pub struct LoopIo {
    /// Stub field we use to generate some messages we're stubbing off.
    player_id: PlayerId,

    /// Input channel for server control messages.
    server_input_rx: mpsc::Receiver<ServerMsg>,

    /// Input ring for client messages from the render/input thread.
    client_input_ring: Arc<input::InputRing>,

    /// Output channel to update the world.
    output_tx: mpsc::Sender<TickMsg>,
}

impl LoopIo {
    pub fn new(
        player_id: PlayerId,
        server_input_rx: mpsc::Receiver<ServerMsg>,
        client_input_ring: Arc<input::InputRing>,
        output_tx: mpsc::Sender<TickMsg>,
    ) -> Self {
        Self {
            player_id,
            server_input_rx,
            client_input_ring,
            output_tx,
        }
    }

    /// Consumes all ready messages from the client.
    pub fn consume_ready_inputs(&mut self) -> TickInput {
        let mut tick_inp = TickInput::new_empty();

        // Read from the client input ring and convert to more realistic scheme.
        let client_inp = self.client_input_ring.take_input();

        let mut pi = PlayerInput::new(self.player_id);
        for act in client_inp.actions() {
            pi.add_block_action(act.data().clone());
        }

        if let Some(pos) = client_inp.new_position() {
            pi.set_move_intent(pos);
        }

        // Now also keep reading from any "server" messages we get.
        loop {
            match self.server_input_rx.try_recv() {
                Ok(m) => match m {
                    ServerMsg::NewPlayer { id, loc } => tick_inp.add_join(PlayerJoin::new(id, loc)),
                },

                Err(TryRecvError::Disconnected) => {
                    tick_inp.mark_closed();
                    break;
                }

                Err(TryRecvError::Empty) => {
                    break;
                }
            }
        }

        tick_inp.add_player_inp(pi);

        tick_inp
    }

    /// Submits a new tick snapshot back to the client.
    pub fn submit_new_tick_state(&mut self, msg: TickMsg) {
        self.output_tx
            .send(msg)
            .expect("tick: failed to submit back changes");
    }
}
